﻿// <copyright file="ServiceOrientedAnalytics.cs" company="Microsoft">
// Copyright (c) 2009 Microsoft All Rights Reserved
// </copyright>
// <author>Microsoft</author>
// <email>mischero@microsoft.com</email>
// <date>2010-04-20</date>
// <summary>Web analytics service behavior class definition</summary>

namespace Microsoft.WebAnalytics.Behaviors
{
    using System;
    using System.ComponentModel;
    using System.ComponentModel.Composition;
    using System.Globalization;
    using System.IO;
    using System.IO.IsolatedStorage;
    using System.Net;
    using System.Runtime.Serialization;
    using System.Text;
    using System.Windows;
    using System.Windows.Interactivity;
    using Microsoft.WebAnalytics;
    
    /// <summary>
    /// The service method for <see cref="Microsoft.WebAnalytics.Behaviors.ServiceOrientedAnalytics"/> 
    /// </summary>
    public enum ServiceMethod
    {
        /// <summary>
        /// Use HTTP Get
        /// </summary>
        Get,

        /// <summary>
        /// Use HTTP Post
        /// </summary>
        Post
    }

    /// <summary>
    /// <img src="../media/Microsoft.WebAnalytics.Behaviors.ServiceOrientedAnalytics.24x24.png"/> 
    /// Track an event to a web analytics service
    /// </summary>
    /// <remarks>See <link xmlns:xlink="http://www.w3.org/1999/xlink" xlink:href="9255fdc2-b6e8-4081-bf08-c172336bf09b">this article</link>
    /// for more information on Service Oriented Analytics.
    /// </remarks>
    public class ServiceOrientedAnalytics : Behavior<FrameworkElement>
    {
        #region Fields
        /// <summary>
        /// The isolated storage setting key for the UserId
        /// </summary>
        private const string UserIdKey = "Microsoft.WebAnalytics.ServiceOrientedAnalytics.UserId";

        /// <summary>
        /// the unique User Id
        /// </summary>
        private string userId;

        /// <summary>
        /// the data contract serializer
        /// </summary>
        private DataContractSerializer serializer = new DataContractSerializer(typeof(AnalyticsEvent));

        #endregion

        #region Constructors
        /// <summary>
        /// Initializes a new instance of the ServiceOrientedAnalytics class.
        /// </summary>
        public ServiceOrientedAnalytics()
        {
            this.Method = ServiceMethod.Get;
            this.Version = "1";
        }

        #endregion

        #region Properties

        /// <summary>
        /// Gets or sets the endpoint for the service 
        /// </summary>
        [TypeConverter(typeof(UriTypeConverter))]
        public Uri Endpoint { get; set; }

        /// <summary>
        /// Gets or sets the account Id
        /// </summary>
        public string AccountId { get; set; }

        /// <summary>
        /// Gets or sets the version of the API
        /// </summary>
        public string Version { get; set; }

        /// <summary>
        /// Gets or sets the service method to use
        /// </summary>
        public ServiceMethod Method { get; set; }

        /// <summary>
        /// Gets or sets the events
        /// </summary>
        [Import]
        public WebAnalyticsEvents Events { get; set; }

        /// <summary>
        /// Gets or sets the web message dispatcher
        /// </summary>
        [Import]
        public Action<WebMessage> Dispatch { get; set; }

        /// <summary>
        /// Gets the unique User id
        /// </summary>
        public string UserId
        {
            get
            {
                if (string.IsNullOrEmpty(this.userId))
                {
#if SILVERLIGHT
                    try
                    {
#if WINDOWS_PHONE
                        if (!IsolatedStorageSettings.ApplicationSettings.TryGetValue(UserIdKey, out this.userId))
                        {
                            this.userId = Guid.NewGuid().ToString();

                            IsolatedStorageSettings.ApplicationSettings[UserIdKey] = this.userId;
                        }
#else
                        if (!IsolatedStorageSettings.SiteSettings.TryGetValue(UserIdKey, out this.userId))
                        {
                            this.userId = Guid.NewGuid().ToString();

                            IsolatedStorageSettings.SiteSettings[UserIdKey] = this.userId;
                        }
#endif
                    }
                    catch (IsolatedStorageException)
                    {
                        this.userId = Guid.NewGuid().ToString();
                    }
#else
                    this.userId = Properties.Settings.Default.ServiceOrientedAnalyticsUserId;

                    if (string.IsNullOrEmpty(this.userId))
                    {
                        this.userId = Guid.NewGuid().ToString();

                        Properties.Settings.Default.ServiceOrientedAnalyticsUserId = this.userId;

                        Properties.Settings.Default.Save();
                    }
#endif
                }

                return this.userId;
            }
        }

        #endregion

        #region Methods
        /// <summary>
        /// Initialize the imported parts and attach the TrackEvent handler
        /// </summary>
        protected override void OnAttached()
        {
            base.OnAttached();

            CompositionInitializerEx.SatisfyImports(this);

            this.Events.TrackEvent += new System.EventHandler<AnalyticsEventArgs>(this.OnTrackEvent);

            this.AssociatedObject.Unloaded += new RoutedEventHandler(delegate(object sender, RoutedEventArgs args)
            {
                System.Windows.Interactivity.Interaction.GetBehaviors(this.AssociatedObject).Remove(this);
            });
        }

        /// <summary>
        /// Detach the TrackEvent() handler
        /// </summary>
        protected override void OnDetaching()
        {
            base.OnDetaching();

            this.Events.TrackEvent -= new System.EventHandler<AnalyticsEventArgs>(this.OnTrackEvent);
        }
        #endregion

        #region Implementation
        /// <summary>
        /// Track the event to the service
        /// </summary>
        /// <param name="sender">the event tracker</param>
        /// <param name="e">the anlalytics event arguments</param>
        private void OnTrackEvent(object sender, AnalyticsEventArgs e)
        {
            System.Diagnostics.Debug.Assert(this.Endpoint != null, "ServiceOrientedAnalytics.Endpoint must be set.");

            switch (this.Method)
            {
                case ServiceMethod.Get:
                    this.GetEvent(e);
                    break;

                case ServiceMethod.Post:
                    this.PostEvent(e);
                    break;
            }
        }

        /// <summary>
        /// Use a GET method and put the parameters in the URL
        /// </summary>
        /// <param name="e">the analytics event argument</param>
        private void GetEvent(AnalyticsEventArgs e)
        {
            var endpoint = this.Endpoint.ToString();

            if (endpoint.EndsWith("/", StringComparison.OrdinalIgnoreCase))
            {
                endpoint = endpoint.Substring(0, endpoint.Length - 1);
            }

            var uriString = string.Format(
                CultureInfo.InvariantCulture,
                "{0}/SOA/{1}/?c={2}&id={3}&n={4}",
                endpoint,
                this.Version,
                System.Convert.ToInt32(e.IsFromCache),
                this.UserId,
                e.AnalyticsEvent.Name);

            var builder = new StringBuilder(uriString);

            if (!string.IsNullOrEmpty(this.AccountId))
            {
                builder.AppendFormat(CultureInfo.InvariantCulture, "&ai={0}", this.AccountId);
            }

            if (!string.IsNullOrEmpty(e.AnalyticsEvent.ObjectName))
            {
                builder.AppendFormat(CultureInfo.InvariantCulture, "&on={0}", e.AnalyticsEvent.ObjectName);
            }

            if (!string.IsNullOrEmpty(e.AnalyticsEvent.ActionValue))
            {
                builder.AppendFormat(CultureInfo.InvariantCulture, "&av={0}", Uri.EscapeUriString(e.AnalyticsEvent.ActionValue));
            }

            if (e.AnalyticsEvent.Value.HasValue)
            {
                builder.AppendFormat(CultureInfo.InvariantCulture, "&v={0}", e.AnalyticsEvent.Value.Value);
            }

            builder.AppendFormat(CultureInfo.InvariantCulture, "&an={0}", e.AnalyticsEvent.AppName);

            if (!string.IsNullOrEmpty(e.AnalyticsEvent.Category))
            {
                builder.AppendFormat(CultureInfo.InvariantCulture, "&ca={0}", e.AnalyticsEvent.Category);
            }

            if (!string.IsNullOrEmpty(e.AnalyticsEvent.ContentId))
            {
                builder.AppendFormat(CultureInfo.InvariantCulture, "&ci={0}", e.AnalyticsEvent.ContentId);
            }

            if (e.AnalyticsEvent.DocumentUri != null)
            {
                builder.AppendFormat(CultureInfo.InvariantCulture, "&d={0}", Uri.EscapeUriString(e.AnalyticsEvent.DocumentUri.ToString()));
            }

            builder.AppendFormat(CultureInfo.InvariantCulture, "&ht={0}", System.Convert.ToInt32(e.AnalyticsEvent.HitType, CultureInfo.InvariantCulture));

#if SILVERLIGHT && !WINDOWS_PHONE
            builder.AppendFormat(CultureInfo.InvariantCulture, "&is={0}", System.Convert.ToInt32(e.AnalyticsEvent.InstallState, CultureInfo.InvariantCulture));
#endif

            builder.AppendFormat(CultureInfo.InvariantCulture, "&fs={0}", System.Convert.ToInt32(e.AnalyticsEvent.IsFullScreen));

            builder.AppendFormat(CultureInfo.InvariantCulture, "&l={0}", e.AnalyticsEvent.Language);

            if (!string.IsNullOrEmpty(e.AnalyticsEvent.NavigationState))
            {
                builder.AppendFormat(CultureInfo.InvariantCulture, "&ns={0}", e.AnalyticsEvent.NavigationState);
            }

            if (!string.IsNullOrEmpty(e.AnalyticsEvent.Referrer))
            {
                builder.AppendFormat(CultureInfo.InvariantCulture, "&re={0}", e.AnalyticsEvent.Referrer);
            }

            builder.AppendFormat(CultureInfo.InvariantCulture, "&rs={0}x{1}", System.Convert.ToInt32(e.AnalyticsEvent.Resolution.Height), System.Convert.ToInt32(e.AnalyticsEvent.Resolution.Width));

            builder.AppendFormat(CultureInfo.InvariantCulture, "&sl={0}", e.AnalyticsEvent.SilverlightVersion);

            if (!string.IsNullOrEmpty(e.AnalyticsEvent.StateName))
            {
                builder.AppendFormat(CultureInfo.InvariantCulture, "&sn={0}", e.AnalyticsEvent.StateName);
            }

            builder.AppendFormat(CultureInfo.InvariantCulture, "&tc={0}", System.Convert.ToInt32(e.AnalyticsEvent.TimeCode.TotalSeconds + 0.5));

            builder.AppendFormat(CultureInfo.InvariantCulture, "&ts={0}", Uri.EscapeUriString(e.AnalyticsEvent.Timestamp.ToString("u", CultureInfo.InvariantCulture)));

            builder.AppendFormat(CultureInfo.InvariantCulture, "&vs={0}", e.AnalyticsEvent.Version);

            builder.AppendFormat(CultureInfo.InvariantCulture, "&z={0}", System.Convert.ToInt32(e.AnalyticsEvent.ZoomFactor * 100));

            uriString = builder.ToString();

            System.Diagnostics.Debug.WriteLine(string.Format(CultureInfo.InvariantCulture, "SOA GET (length={0}): {1}", uriString.Length, uriString));

            var address = new Uri(uriString, UriKind.Absolute);

            var client = new WebClient();

            client.DownloadStringAsync(address);
        }

        /// <summary>
        /// Use an HTTP Post event to and put the data in the body of the message
        /// </summary>
        /// <param name="e">the anaytics event arguments</param>
        private void PostEvent(AnalyticsEventArgs e)
        {
            var builder = new StringBuilder();

            builder.AppendFormat(
                CultureInfo.InvariantCulture,
                "{0}/SOA/{1}/?c={2}&id={3}",
                this.Endpoint.ToString(),
                this.Version,
                System.Convert.ToInt32(e.IsFromCache),
                this.UserId);

            if (!string.IsNullOrEmpty(this.AccountId))
            {
                builder.AppendFormat(
                    CultureInfo.InvariantCulture,
                    "&ai={0}",
                    this.AccountId);
            }

            var requestUriString = builder.ToString();

            var request = WebRequest.Create(requestUriString);

            request.ContentType = "application/xml";

            request.Method = "POST";

            var requestStream = new RequestStream
            {
                AnalyticsEvent = e.AnalyticsEvent,
                Request = request
            };

            request.BeginGetRequestStream(new AsyncCallback(this.OnRequestStream), requestStream);
        }

        /// <summary>
        /// Serialize the Analytics event in an XML Stream
        /// </summary>
        /// <param name="asyncResult">the async result</param>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "This code shouldn't throw exception.")]
        private void OnRequestStream(IAsyncResult asyncResult)
        {
            var requestStream = asyncResult.AsyncState as RequestStream;

            try
            {
                using (var stream = requestStream.Request.EndGetRequestStream(asyncResult))
                {
                    this.serializer.WriteObject(stream, requestStream.AnalyticsEvent);
                }
            }
            catch (System.Exception se)
            {
                System.Diagnostics.Debug.WriteLine(se.Message);
            }

            requestStream.Request.BeginGetResponse(new AsyncCallback(this.OnPosted), requestStream.Request);
        }

        /// <summary>
        /// Event handler called when the Analtyics Event is posted to the service
        /// </summary>
        /// <param name="asyncResult">the async result</param>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "This code shouldn't throw exceptions.")]
        private void OnPosted(IAsyncResult asyncResult)
        {
            try
            {
                var request = asyncResult.AsyncState as WebRequest;

                var response = request.EndGetResponse(asyncResult);

                using (var stream = response.GetResponseStream())
                {
                    StreamReader reader = new StreamReader(stream);

                    System.Diagnostics.Debug.WriteLine(reader.ReadToEnd());
                }
            }
            catch (System.Exception se)
            {
                System.Diagnostics.Debug.WriteLine(se.Message);
            }
        }

        /// <summary>
        /// The request stream
        /// </summary>
        private class RequestStream
        {
            /// <summary>
            /// Gets or sets the web request
            /// </summary>
            public WebRequest Request { get; set; }

            /// <summary>
            /// Gets or sets the analytics event
            /// </summary>
            public AnalyticsEvent AnalyticsEvent { get; set; }
        }
        #endregion
    }
}
